/*
 * Cartograph main() definition.
 *
 * Written by T.Pierron, Dec 2010.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "SIT.h"
#include "graphics.h"
#include "Cartograph.h"
#include "Level.h"
#include "NBT.h"
#include "ViewImage.h"
#include "png.h"

Options  opt;
ListHead levels;
Mutex    serial;
Mutex    sync;
Thread   worker;

/* User hit render button: start process in an async thread */
static int render(SIT_Widget w, APTR cd, APTR ud)
{
	if (! worker)
	{
		int ws;

		/* First: gather options value */
		SIT_GetValues(SIT_GetById(ud, "day"), SIT_SliderPos, &opt.daynight, NULL);
		SIT_GetValues(SIT_GetById(ud, "noise"), SIT_SliderPos, &opt.tex, NULL);
		opt.notify = ud;
		opt.cancel = False;
		opt.total  = opt.cur = 0;

		SIT_GetValues(ud, SIT_Width, &ws, NULL);
		SIT_SetValues(w,  SIT_Title, "STOP", NULL);
		SIT_SetValues(SIT_GetById(ud, "prog"), SIT_ProgressPos, 0, NULL);
		w = SIT_GetById(ud, "preview");
		if (opt.image) {
			SIT_SetValues(w, VIT_Image, NULL, NULL);
			GFX_FreeImage(opt.image), opt.image = NULL;
		}

		SIT_SetValues(w, SIT_Visible, True, SIT_Height, ws/2, NULL);
		SIT_Refresh(w, 0, 0, 0, 0, True);

		worker = ThreadCreate(LEVEL_Render, NULL);
	}
	else
	{
		opt.cancel = True;
		MutexEnter(sync); // Wait for thread completition
		SIT_SetValues(w,  SIT_Title, "RENDER!", NULL);
		SIT_SetValues(SIT_GetById(ud, "preview"), SIT_Visible, False, NULL);
		worker = 0;
		MutexLeave(sync);
	}
	return 1;
}

Image resize(Image src, int w, int h);
int save_as_png(Image src, STRPTR dest);

static STRPTR get_image_name(void)
{
	static STRPTR modeName[] = {
		NULL, "heightmap", "heightgray", "slice", "oblique", "obliqueangle", "isometric"
	};
	static STRPTR timeName[] = {
		"day", "daynight", "night"
	};
	static TEXT buffer[256];

	CopyString(buffer, opt.name, sizeof buffer);
	if (opt.mode > NORMAL)
		StrCatM(buffer, sizeof buffer, '-', modeName[opt.mode], NULL);

	if (opt.rotate)
		StrCatM(buffer, sizeof buffer, '-', "rotated", NULL);

	if (opt.cave)
		StrCatM(buffer, sizeof buffer, '-', "cave", NULL);

	StrCatM(buffer, sizeof buffer, '-', timeName[opt.daynight], NULL);

	time_t      now = time(NULL);
	struct tm * tm  = localtime(&now);

	sprintf(strchr(buffer, 0), "-%d-%02d-%02d.png", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday);

	return buffer;
}

/* thread notifies us on its progress */
static int update(SIT_Widget w, APTR stage, APTR ud)
{
	static SIT_Widget prv, prg;
	Image src = opt.image;

	if (prv == NULL)
		prv = SIT_GetById(w, "preview"),
		prg = SIT_GetById(w, "prog");

	switch ((ULONG) stage) {
	case 0: /* Start */
		SIT_SetValues(prv, VIT_Image, src, NULL);
		break;
	case 1: /* At least one chunk processed */
		if (src)
		{
			int pos, npos;
			SIT_GetValues(prg, SIT_ProgressPos, &pos, NULL);
			npos = opt.cur * 100 / opt.total;
			if (npos != pos) /* Avoid doing an update for every chunk processed */
			{
				SIT_SetValues(prg, SIT_ProgressPos, npos, NULL);
				ViewImageInvalidate(prv, 0, 0, 0, 0);
			}
		}
		break;
	case 2: /* Thread has completed its work */
		worker = 0;
		SIT_SetValues(SIT_GetById(w, "render"),  SIT_Title, "RENDER!", NULL);
		SIT_SetValues(SIT_GetById(ud, "prog"), SIT_ProgressPos, 0, NULL);
		if (! opt.cancel) save_as_png(src, get_image_name());
	}
	return 1;
}

/* Main dialog is about to be closed */
static int close(SIT_Widget w, APTR deny, APTR ud)
{
	if (worker)
	{
		opt.cancel = 1;
		MutexEnter(sync);
		MutexLeave(sync);
	}
	/* Save configuration */
	if (opt.name)
		SetINIValue(CONFIG, "World", opt.name);
	SetINIValueInt(CONFIG, "RenderMode", opt.mode);
	SetINIValueInt(CONFIG, "CaveOnly", opt.cave);
	SetINIValueInt(CONFIG, "IgnoreWater", opt.nowater);
	SetINIValueInt(CONFIG, "Flip", opt.flip);
	SetINIValueInt(CONFIG, "Rotate", opt.rotate);
//	SetINIValueInt(CONFIG, "Exclude", opt.exclude);
	SetINIValueInt(CONFIG, "DayNight", opt.daynight);
	SetINIValueInt(CONFIG, "Noise", opt.tex);
	SetINIValueInt(CONFIG, "Slice", opt.slice);
	return 1;
}

/* User double-clicked on a block id */
static int select_id(SIT_Widget w, APTR row, APTR parent)
{
	int n = (ULONG) row;
	SIT_GetValues(w, SIT_RowTag(n), &row, NULL);
	opt.exclude = (int) row;
	SIT_SetValues(SIT_GetById(parent, "exclude"), SIT_ScrollPos, &opt.exclude, NULL);
	SIT_CloseDialog(w);
	return 1;
}

static int chooseid(SIT_Widget w, APTR cd, APTR list)
{
	if (list) /* Select button */
	{
		int row;
		SIT_GetValues(list, SIT_SelectedIndex, &row, NULL);
		if (row >= 0)
			SIT_ApplyCallback(list, (APTR) row, SITE_OnActivate);
	}
	else SIT_CloseDialog(w);
	return 1;
}

/* Show a list to the user to select a block ID */
static int select_block(SIT_Widget w, APTR deny, APTR parent)
{
	SIT_Widget dialog = SIT_CreateWidget("SelectID", SIT_DIALOG, parent,
		SIT_Title,      "Select block ID",
		SIT_DialogType, SITV_Modal,
		SIT_Margins,    10, 10, 10, 10,
		NULL
	);

	SIT_CreateWidgets(dialog,
		"<label width=250 name=help title='Select a block ID in the list below."
		" Only the selected block type will be included in the generated map:' left=FORM right=FORM>"
		"<listbox name=blocks listBoxFlags=", SITV_FullRowSelect, "columnWidths='*\t50' columnNames='Name\tID'"
		" columnAlign='L\tR' left=FORM right=FORM top=WIDGET,help,6 height=150>"
		"<button name=Select buttonType=", SITV_DefaultButton, "left=FORM bottom=FORM margins=0,10,0,10>"
		"<button name=Cancel buttonType=", SITV_CancelButton, "right=FORM bottom=FORM margins=0,10,0,10>"
	);
	SIT_SetAttributes(dialog, "<blocks bottom=WIDGET,Select,6>");
	SIT_Widget list = SIT_GetById(dialog, "blocks");

	Level lev = LEVEL_Init();

	int i;
	for (i = 1; i < 255; i ++)
	{
		STRPTR name = NBT_FindItemName(i);
		TEXT   num[10];
		if (strncmp(name, "Unknown", 7)) {
			Image icon;
			DATA8 p;
			int   idx = LEVEL_IDToTerrain(i);
			if (idx < 0) idx = -idx;
			icon = GFX_CreateImage(16, 16, 32);
			if (idx > 0 || i == 2)
			{
				memcpy(icon->bitmap, lev->texture[idx] + 1, 4*16*16);
				for (p = icon->bitmap + 4*16*46-4; p > icon->bitmap; p -= 4)
					swap(p[0], p[2]);
			}
			else /* Dummy icon */
			{
				for (p = icon->bitmap, idx = 0; idx < 16*16; p += 4, idx++)
					memcpy(p, (idx>>4) == 0 || (idx & 0xf) == 0 ? "\x7f\x3f\x6b\xff" : "\xff\x7f\xd6\xff", 4);
			}
			sprintf(num, "%d", i);
			SIT_ListInsertItem(list, -1, icon, (APTR) i, name, num, NULL);
			GFX_FreeImage(icon);
		}
	}
	SIT_AddCallback(list, SITE_OnActivate, select_id, parent);
	SIT_AddCallback(SIT_GetById(dialog, "Select"), SITE_OnActivate, chooseid, list);
	SIT_AddCallback(SIT_GetById(dialog, "Cancel"), SITE_OnActivate, chooseid, 0);

	SIT_ManageWidget(dialog);
	return 1;
}

/* Enable 'sliceval' if 'slice' is selected */
static int enable(SIT_Widget w, APTR ena, APTR sliceval)
{
	SIT_SetValues(sliceval, SIT_Enabled, ena > 0, NULL);
	return 1;
}

static int sort_files(ListNode * n1, ListNode * n2)
{
	World w1 = (APTR) n1;
	World w2 = (APTR) n2;

	int res = strcasecmp(w1->name, w2->name);

	/* Two worlds with same name - add folder name (which is obviously always different) */
	if (res == 0)
	{
		STRPTR p = strchr(w1->name, 0); *p = ' ';
		p = strchr(w2->name, 0); *p = ' ';
		res = strcasecmp(w1->name, w2->name);
	}
	return res;
}

static int add_world(STRPTR dir, STRPTR file, APTR ud)
{
	STRPTR name = NULL;
	AddPart(dir, file, 512);
	AddPart(dir, "level.dat", 512);
	/* Try to get the level name from the NBT */
	if (FileExists(dir))
	{
		NBT dat = NBT_Parse(dir);
		ParentDir(dir);
		if (dat)
		{
			NBT node = NBT_FindNode(dat, "LevelName");
			if (node) name = node->val.string;
		}
		if (name == NULL) name = file;

		World lev = malloc(StrLenM(dir, name, file, NULL) + sizeof *lev + 6);
		AssignStrings(&lev->path, lev + 1, dir, name, NULL);
		sprintf(lev->name, "%s%c(%s)", name, 0, file);
		ParentDir(dir);
		if (dat) NBT_Free(dat);
		ListInsertSort(&levels, &lev->node, sort_files);
	}
	else ParentDir(dir), ParentDir(dir);
	return 1;
}

static int set_level(SIT_Widget w, APTR cd, APTR ud)
{
	World lev;
	SIT_GetValues(w, SIT_RowTag(cd), &lev, NULL);
	opt.world = lev->path;
	opt.name  = lev->name;
	return 1;
}

static void add_worlds(SIT_Widget list)
{
	TEXT   dir[512];
	STRPTR appdata = getenv("APPDATA");

	if (! appdata) return;

	CopyString(dir, appdata, sizeof dir);
	AddPart(dir, ".minecraft/saves", sizeof dir);
	ScanDir(dir, add_world, list);

	World lev;

	for (lev = HEAD(levels); lev; NEXT(lev)) {
		int idx = SIT_ListInsertItem(list, -1, NULL, lev, lev->name, NULL);
		if (opt.name && strcasecmp(lev->name, opt.name) == 0)
			SIT_SetValues(list, SIT_SelectedIndex, idx, NULL), opt.world = lev->path;
	}

	SIT_AddCallback(list, SITE_OnChange, set_level, NULL);
}

static int show_zoom(SIT_Widget w, APTR gc, APTR ud)
{
	static TEXT num[16];
	static double old = 0;
	double fact;
	SIT_GetValues(w, VIT_Factor, &fact, NULL);
	if (old != fact) sprintf(num, "%.5g", old = fact);
	GFX_SetDrMd(gc, GFX_TxtModeTransparent);
	GFX_SetTextColor(gc, 0);
	GFX_Text(gc, num, -1, 5, 6);
	GFX_SetTextColor(gc, RGB(255,255,255));
	GFX_Text(gc, num, -1, 5, 5);
	return 1;
}

/* MAIN Definition */
int my_main(int nb, char * argv[])
{
/*	if (nb > 1)
	{
		opt.world = argv[1];

		LEVEL_Render("out.png");
		return 0;
	} */

	STRPTR file;

	if (! FileExists(file = "WLF.png") || ! FileExists(file = "texture.png"))
	{
		SIT_Log(SIT_ERROR,
			"Cannot find %s!\n\nThis file is essential for this program to work. You can get it here:\n"
			"https://code.google.com/p/minecraft-carto/downloads/list", file
		);
		return 1;
	}

	SIT_Widget app = SIT_CreateWidget("MinecraftCarto", SIT_APP, NULL, NULL);

	SIT_Widget dialog = SIT_CreateWidget("MainWnd", SIT_DIALOG, app,
		SIT_Title,   "Cartograph",
		SIT_Styles,  SITV_NoResize,
		SIT_Margins, 10, 10, 10, 10,
		NULL
	);

	/* Quote initial value from ini file */
	INIFile ini = ParseINI(CONFIG);

	if (ini)
	{
		opt.name     = GetINIValueStr(ini, "World", NULL);
		opt.mode     = GetINIValueInt(ini, "RenderMode", 0);
		opt.cave     = GetINIValueInt(ini, "CaveOnly", 0);
		opt.nowater  = GetINIValueInt(ini, "IgnoreWater", 0);
		opt.flip     = GetINIValueInt(ini, "Flip", 0);
		opt.rotate   = GetINIValueInt(ini, "Rotate", 1);
		opt.exclude  = GetINIValueInt(ini, "Exclude", 0);
		opt.daynight = GetINIValueInt(ini, "DayNight", 0);
		opt.tex      = GetINIValueInt(ini, "Noise", 0);
		opt.slice    = GetINIValueInt(ini, "Slice", 0);
		if (opt.name) opt.name = strdup(opt.name);
		FreeINI(ini);
	}
	else opt.rotate = 1;

	serial = MutexCreate();

	SIT_CreateWidgets(dialog,
		"<label name=title title='Cartograph v" VERSION "' font='Arial/bold/italic/150%'>"
		"<label name=author title='by ZomBuster && T.Pierron' top=WIDGET,title,5>"
		"<frame name=world title=World top=WIDGET,author,5 margins=10,10,10,10>"
		"  <listbox name=worlds listBoxFlags=", SITV_FullRowSelect, "columnWidths=* columnNames='Name' width=150 top=FORM"
		"   bottom=FORM left=FORM right=FORM>"
		"</frame>"
		"<frame name=mode title=Mode left=WIDGET,world,20 top=WIDGET,author,5 margins=10,10,10,10>"
		"  <button name=normal     curValue=", &opt.mode, "buttonType=", SITV_RadioButton, "title='Normal'>"
		"  <button name=heightmap  curValue=", &opt.mode, "buttonType=", SITV_RadioButton, "title='Heightmap'"
		"   top=WIDGET,normal,6>"
		"  <button name=heightgray curValue=", &opt.mode, "buttonType=", SITV_RadioButton, "title='Heightgray'"
		"   top=WIDGET,heightmap,6>"
		"  <button name=slice      curValue=", &opt.mode, "buttonType=", SITV_RadioButton, "title='Slice'"
		"   top=WIDGET,heightgray,6>"
		"  <button name=oblique    curValue=", &opt.mode, "buttonType=", SITV_RadioButton, "title='Oblique' top=WIDGET,slice,6>"
		"  <button name=obliqueang curValue=", &opt.mode, "buttonType=", SITV_RadioButton, "title='Oblique-angled'"
		"   top=WIDGET,oblique,6>"
		"  <button name=isometric  curValue=", &opt.mode, "buttonType=", SITV_RadioButton, "title='Isometric'"
		"   top=WIDGET,obliqueang,6>"
		"  <editbox name=sliceval maxValue=128 editType=", SITV_Integer, " left=OPPOSITE,normal,0 top=WIDGET,isometric,6"
		"   width=50 title=0 scrollPos=", &opt.slice, "enabled=", opt.mode==SLICE, ">"
		"  <label name='0-128' top=MIDDLE,sliceval left=WIDGET,sliceval,5 margins=10,10,10,10>"
		"  <editbox name=exclude maxValue=255 editType=", SITV_Integer, "left=OPPOSITE,normal,0 top=WIDGET,sliceval,6"
		"   width=50, title=0 scrollPos=", &opt.exclude, ">"
		"  <label name=getblk title='Only (<a id=\"0\">?</a>)' top=MIDDLE,exclude left=WIDGET,exclude,5 HTMLLink=1>"
		"</frame>"
		"<frame name=options title=Options left=WIDGET,mode,20 top=WIDGET,author,5>"
		"  <button name=nowater  buttonType=", SITV_CheckBox, "title='Ignore water' curValue=", &opt.nowater, ">"
		"  <button name=flip     buttonType=", SITV_CheckBox, "title=Flip top=WIDGET,nowater,6 curValue=", &opt.flip, ">"
		"  <button name=rotate   buttonType=", SITV_CheckBox, "title=Rotate top=WIDGET,flip,6 curValue=", &opt.rotate, ">"
		"  <button name=cavemode buttonType=", SITV_CheckBox, "title='Cave Mode' top=WIDGET,rotate,6 curValue=", &opt.cave, ">"
		"  <button name=invmode  buttonType=", SITV_CheckBox, "title='Inventory' top=WIDGET,cavemode,6 curValue=",
		&opt.inventory, "enabled", opt.mode==NORMAL, ">"
		"  <label name=labday title='Day:'>"
		"  <slider name=day maxValue=2 tickFreq=1 top=WIDGET,invmode,6 left=WIDGET,labday width=100 sliderPos=", opt.daynight, ">"
		"  <label name=labnight title='Night' left=WIDGET,day top=MIDDLE,day>"
		"  <label name=labflat title='Flat:'>"
		"  <slider name=noise maxValue=8 tickFreq=1 top=WIDGET,day left=WIDGET,labflat width=100 sliderPos=", opt.tex, ">"
		"  <label name=labnoise title='Noise' left=WIDGET,noise top=MIDDLE,noise>"
		"</frame>"
		"<button name=render title='RENDER!' left=WIDGET,options,20 top=WIDGET,author,5 resizePolicy=", SITV_Fixed,
		" bottom=OPPOSITE,mode margins=0,20,0,20>"
		"<progress title='%d%%' name=prog left=FORM right=OPPOSITE,render,0 top=WIDGET,mode,10>"
		"<canvas name=preview left=FORM right=FORM top=WIDGET,prog,10 frameStyle=", SITV_Sunken, " background=0 visible=0>"
	);
	SIT_SetAttributes(dialog,
		"<title left=FORM right=OPPOSITE,render>"
		"<world bottom=OPPOSITE,mode>"
		"<labday top=MIDDLE,day>"
		"<labflat top=MIDDLE,noise>"
	);

	add_worlds(SIT_GetById(dialog, "worlds"));

	SIT_AddCallback(SIT_GetById(dialog, "render"),  SITE_OnActivate, render, dialog);
	SIT_AddCallback(SIT_GetById(dialog, "getblk"),  SITE_OnActivate, select_block, dialog);
	SIT_AddCallback(SIT_GetById(dialog, "slice"),   SITE_OnChanged,  enable, SIT_GetById(dialog, "sliceval"));
	SIT_AddCallback(SIT_GetById(dialog, "normal"),  SITE_OnChanged,  enable, SIT_GetById(dialog, "invmode"));
	SIT_AddCallback(dialog, SITE_OnUser, update, dialog);
	SIT_AddCallback(dialog, SITE_OnClose, close, NULL);
	SIT_Widget c = SIT_GetById(dialog, "preview");
	ViewImageInit(c, NULL);
	SIT_SetValues(c, VIT_Overlay, show_zoom, NULL);

	SIT_ManageWidget(dialog);

	return SIT_Main();
}

int save_as_png(Image src, STRPTR dest)
{
	FILE * out = fopen(dest, "wb");

	if (out)
	{
		png_structp png_ptr  = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
		png_infop   info_ptr = png_create_info_struct(png_ptr);
		png_bytep   row;
		int         i, j;

		if (png_ptr == NULL || info_ptr == NULL)
			return 0;

		/* Use custom callback for io */
		png_set_compression_level(png_ptr, 6);
		png_init_io(png_ptr, out);

		if (setjmp(png_jmpbuf(png_ptr)))
		{
			fclose(out);
			png_destroy_write_struct(&png_ptr, &info_ptr);
			return 0;
		}

		png_set_IHDR(png_ptr, info_ptr, src->width, src->height, 8, PNG_COLOR_TYPE_RGBA,
			PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE
		);

		png_write_info(png_ptr, info_ptr);

		/* Do not alter original, it is still displayed */
		DATA8 line = malloc(src->stride);

		/* Write bytes */
		for (row = src->bitmap, i = 0; i < src->height; i ++, row += src->stride)
		{
			/* Convert win32 bitmap BGRA into RGBA */
			DATA8 p;
			memcpy(line, row, src->stride);
			for (p = line, j = 0; j < src->stride; j += 4, p += 4)
				swap(p[0], p[2]);
			png_write_row(png_ptr, line);
		}
		free(line);

		/* End write */
		png_write_end(png_ptr, info_ptr);
		png_destroy_write_struct(&png_ptr, &info_ptr);
		fclose(out);
		return 1;
	}
	else SIT_Log(SIT_ERROR, "Cannot write file '%s': %s\n", dest, GetError());
	return 0;
}
